﻿;;  An environment is an atom referencing a map where keys are strings
;;  instead of symbols.  The outer environment is the value associated
;;  with the normally invalid :outer key.

;;  Private helper for new-env.
(def! bind-env (fn* [env b e]
  (if (empty? b)
    (if (empty? e)
      env
      (throw "too many arguments in function call"))
    (let* [b0 (first b)]
      (if (= '& b0)
        (if (= 2 (count b))
          (if (symbol? (nth b 1))
            (assoc env (str (nth b 1)) e)
            (throw "formal parameters must be symbols"))
          (throw "misplaced '&' construct"))
        (if (empty? e)
          (throw "too few arguments in function call")
          (if (symbol? b0)
            (bind-env (assoc env (str b0) (first e)) (rest b) (rest e))
            (throw "formal parameters must be symbols"))))))))

(def! new-env (fn* [& args]
  (if (<= (count args) 1)
    (atom {:outer (first args)})
    (atom (apply bind-env {:outer (first args)} (rest args))))))

(def! env-as-map (fn* [env]
  (dissoc @env :outer)))

(def! env-get-or-nil (fn* [env k]
  (let* [ks (str k)
         e (env-find-str env ks)]
    (if e
      (get @e ks)))))

;;  Private helper for env-get and env-get-or-nil.
(def! env-find-str (fn* [env ks]
  (if env
    (let* [data @env]
      (if (contains? data ks)
        env
        (env-find-str (get data :outer) ks))))))

(def! env-get (fn* [env k]
  (let* [ks (str k)
         e (env-find-str env ks)]
    (if e
      (get @e ks)
      (throw (str "'" ks "' not found"))))))

;;  The return value must be ignored.
(def! env-set (fn* [env k v]
  (swap! env assoc (str k) v)))